/* *
 *
 *  (c) 2016 Highsoft AS
 *  Authors: Jon Arild Nygard
 *
 *  License: www.highcharts.com/license
 *
 *  !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
 *
 * */
/* eslint no-console: 0 */
"use strict";
import H from "../parts/Globals.js";
import U from "../parts/Utilities.js";
var defined = U.defined,
  extend = U.extend,
  isNumber = U.isNumber,
  isString = U.isString,
  pick = U.pick,
  wrap = U.wrap;
import "./GridAxis.js";
import Tree from "./Tree.js";
import mixinTreeSeries from "../mixins/tree-series.js";
import "../modules/broken-axis.src.js";
var addEvent = H.addEvent,
  argsToArray = function (args) {
    return Array.prototype.slice.call(args, 1);
  },
  find = H.find,
  fireEvent = H.fireEvent,
  getLevelOptions = mixinTreeSeries.getLevelOptions,
  merge = H.merge,
  isBoolean = function (x) {
    return typeof x === "boolean";
  },
  isObject = function (x) {
    // Always use strict mode.
    return U.isObject(x, true);
  },
  GridAxis = H.Axis,
  GridAxisTick = H.Tick;
var override = function (obj, methods) {
  var method, func;
  for (method in methods) {
    if (Object.hasOwnProperty.call(methods, method)) {
      func = methods[method];
      wrap(obj, method, func);
    }
  }
};
var getBreakFromNode = function (node, max) {
  var from = node.collapseStart,
    to = node.collapseEnd;
  // In broken-axis, the axis.max is minimized until it is not within a break.
  // Therefore, if break.to is larger than axis.max, the axis.to should not
  // add the 0.5 axis.tickMarkOffset, to avoid adding a break larger than
  // axis.max
  // TODO consider simplifying broken-axis and this might solve itself
  if (to >= max) {
    from -= 0.5;
  }
  return {
    from: from,
    to: to,
    showPoints: false,
  };
};
/**
 * Creates a list of positions for the ticks on the axis. Filters out positions
 * that are outside min and max, or is inside an axis break.
 *
 * @private
 * @function getTickPositions
 *
 * @param {Highcharts.Axis} axis
 *        The Axis to get the tick positions from.
 *
 * @return {Array<number>}
 *         List of positions.
 */
var getTickPositions = function (axis) {
  return Object.keys(axis.mapOfPosToGridNode).reduce(function (arr, key) {
    var pos = +key;
    if (axis.min <= pos && axis.max >= pos && !axis.isInAnyBreak(pos)) {
      arr.push(pos);
    }
    return arr;
  }, []);
};
/**
 * Check if a node is collapsed.
 *
 * @private
 * @function isCollapsed
 *
 * @param {Highcharts.Axis} axis
 *        The axis to check against.
 *
 * @param {object} node
 *        The node to check if is collapsed.
 *
 * @param {number} pos
 *        The tick position to collapse.
 *
 * @return {boolean}
 *         Returns true if collapsed, false if expanded.
 */
var isCollapsed = function (axis, node) {
  var breaks = axis.options.breaks || [],
    obj = getBreakFromNode(node, axis.max);
  return breaks.some(function (b) {
    return b.from === obj.from && b.to === obj.to;
  });
};
/**
 * Calculates the new axis breaks to collapse a node.
 *
 * @private
 * @function collapse
 *
 * @param {Highcharts.Axis} axis
 *        The axis to check against.
 *
 * @param {object} node
 *        The node to collapse.
 *
 * @param {number} pos
 *        The tick position to collapse.
 *
 * @return {Array<object>}
 *         Returns an array of the new breaks for the axis.
 */
var collapse = function (axis, node) {
  var breaks = axis.options.breaks || [],
    obj = getBreakFromNode(node, axis.max);
  breaks.push(obj);
  return breaks;
};
/**
 * Calculates the new axis breaks to expand a node.
 *
 * @private
 * @function expand
 *
 * @param {Highcharts.Axis} axis
 *        The axis to check against.
 *
 * @param {object} node
 *        The node to expand.
 *
 * @param {number} pos
 *        The tick position to expand.
 *
 * @return {Array<object>}
 *         Returns an array of the new breaks for the axis.
 */
var expand = function (axis, node) {
  var breaks = axis.options.breaks || [],
    obj = getBreakFromNode(node, axis.max);
  // Remove the break from the axis breaks array.
  return breaks.reduce(function (arr, b) {
    if (b.to !== obj.to || b.from !== obj.from) {
      arr.push(b);
    }
    return arr;
  }, []);
};
/* eslint-disable valid-jsdoc */
/**
 * Calculates the new axis breaks after toggling the collapse/expand state of a
 * node. If it is collapsed it will be expanded, and if it is exapended it will
 * be collapsed.
 *
 * @private
 * @function toggleCollapse
 *
 * @param {Highcharts.Axis} axis
 *        The axis to check against.
 *
 * @param {object} node
 *        The node to toggle.
 *
 * @return {Array<object>}
 *         Returns an array of the new breaks for the axis.
 */
var toggleCollapse = function (axis, node) {
  return isCollapsed(axis, node) ? expand(axis, node) : collapse(axis, node);
};
var renderLabelIcon = function (tick, params) {
  var icon = tick.labelIcon,
    isNew = !icon,
    renderer = params.renderer,
    labelBox = params.xy,
    options = params.options,
    width = options.width,
    height = options.height,
    iconCenter = {
      x: labelBox.x - width / 2 - options.padding,
      y: labelBox.y - height / 2,
    },
    rotation = params.collapsed ? 90 : 180,
    shouldRender = params.show && isNumber(iconCenter.y);
  if (isNew) {
    tick.labelIcon = icon = renderer
      .path(renderer.symbols[options.type](options.x, options.y, width, height))
      .addClass("highcharts-label-icon")
      .add(params.group);
  }
  // Set the new position, and show or hide
  if (!shouldRender) {
    icon.attr({ y: -9999 }); // #1338
  }
  // Presentational attributes
  if (!renderer.styledMode) {
    icon
      .attr({
        "stroke-width": 1,
        fill: pick(params.color, "#666666"),
      })
      .css({
        cursor: "pointer",
        stroke: options.lineColor,
        strokeWidth: options.lineWidth,
      });
  }
  // Update the icon positions
  icon[isNew ? "attr" : "animate"]({
    translateX: iconCenter.x,
    translateY: iconCenter.y,
    rotation: rotation,
  });
};
var onTickHover = function (label) {
  label.addClass("highcharts-treegrid-node-active");
  if (!label.renderer.styledMode) {
    label.css({
      textDecoration: "underline",
    });
  }
};
var onTickHoverExit = function (label, options) {
  var css = defined(options.style) ? options.style : {};
  label.removeClass("highcharts-treegrid-node-active");
  if (!label.renderer.styledMode) {
    label.css({
      textDecoration: css.textDecoration,
    });
  }
};
/**
 * Creates a tree structure of the data, and the treegrid. Calculates
 * categories, and y-values of points based on the tree.
 *
 * @private
 * @function getTreeGridFromData
 *
 * @param {Array<*>} data
 *        All the data points to display in the axis.
 *
 * @param {boolean} uniqueNames
 *        Wether or not the data node with the same name should share grid cell.
 *        If true they do share cell. False by default.
 *
 * @param {number} numberOfSeries
 *
 * @return {object}
 *         Returns an object containing categories, mapOfIdToNode,
 *         mapOfPosToGridNode, and tree.
 *
 * @todo There should be only one point per line.
 * @todo It should be optional to have one category per point, or merge cells
 * @todo Add unit-tests.
 */
var getTreeGridFromData = function (data, uniqueNames, numberOfSeries) {
  var categories = [],
    collapsedNodes = [],
    mapOfIdToNode = {},
    mapOfPosToGridNode = {},
    posIterator = -1,
    uniqueNamesEnabled = isBoolean(uniqueNames) ? uniqueNames : false,
    tree,
    treeParams,
    updateYValuesAndTickPos;
  // Build the tree from the series data.
  treeParams = {
    // After the children has been created.
    after: function (node) {
      var gridNode = mapOfPosToGridNode[node.pos],
        height = 0,
        descendants = 0;
      gridNode.children.forEach(function (child) {
        descendants += child.descendants + 1;
        height = Math.max(child.height + 1, height);
      });
      gridNode.descendants = descendants;
      gridNode.height = height;
      if (gridNode.collapsed) {
        collapsedNodes.push(gridNode);
      }
    },
    // Before the children has been created.
    before: function (node) {
      var data = isObject(node.data) ? node.data : {},
        name = isString(data.name) ? data.name : "",
        parentNode = mapOfIdToNode[node.parent],
        parentGridNode = isObject(parentNode)
          ? mapOfPosToGridNode[parentNode.pos]
          : null,
        hasSameName = function (x) {
          return x.name === name;
        },
        gridNode,
        pos;
      // If not unique names, look for a sibling node with the same name.
      if (
        uniqueNamesEnabled &&
        isObject(parentGridNode) &&
        !!(gridNode = find(parentGridNode.children, hasSameName))
      ) {
        // If if there is a gridNode with the same name, reuse position.
        pos = gridNode.pos;
        // Add data node to list of nodes in the grid node.
        gridNode.nodes.push(node);
      } else {
        // If it is a new grid node, increment position.
        pos = posIterator++;
      }
      // Add new grid node to map.
      if (!mapOfPosToGridNode[pos]) {
        mapOfPosToGridNode[pos] = gridNode = {
          depth: parentGridNode ? parentGridNode.depth + 1 : 0,
          name: name,
          nodes: [node],
          children: [],
          pos: pos,
        };
        // If not root, then add name to categories.
        if (pos !== -1) {
          categories.push(name);
        }
        // Add name to list of children.
        if (isObject(parentGridNode)) {
          parentGridNode.children.push(gridNode);
        }
      }
      // Add data node to map
      if (isString(node.id)) {
        mapOfIdToNode[node.id] = node;
      }
      // If one of the points are collapsed, then start the grid node in
      // collapsed state.
      if (data.collapsed === true) {
        gridNode.collapsed = true;
      }
      // Assign pos to data node
      node.pos = pos;
    },
  };
  updateYValuesAndTickPos = function (map, numberOfSeries) {
    var setValues = function (gridNode, start, result) {
      var nodes = gridNode.nodes,
        end = start + (start === -1 ? 0 : numberOfSeries - 1),
        diff = (end - start) / 2,
        padding = 0.5,
        pos = start + diff;
      nodes.forEach(function (node) {
        var data = node.data;
        if (isObject(data)) {
          // Update point
          data.y = start + data.seriesIndex;
          // Remove the property once used
          delete data.seriesIndex;
        }
        node.pos = pos;
      });
      result[pos] = gridNode;
      gridNode.pos = pos;
      gridNode.tickmarkOffset = diff + padding;
      gridNode.collapseStart = end + padding;
      gridNode.children.forEach(function (child) {
        setValues(child, end + 1, result);
        end = child.collapseEnd - padding;
      });
      // Set collapseEnd to the end of the last child node.
      gridNode.collapseEnd = end + padding;
      return result;
    };
    return setValues(map["-1"], -1, {});
  };
  // Create tree from data
  tree = Tree.getTree(data, treeParams);
  // Update y values of data, and set calculate tick positions.
  mapOfPosToGridNode = updateYValuesAndTickPos(
    mapOfPosToGridNode,
    numberOfSeries
  );
  // Return the resulting data.
  return {
    categories: categories,
    mapOfIdToNode: mapOfIdToNode,
    mapOfPosToGridNode: mapOfPosToGridNode,
    collapsedNodes: collapsedNodes,
    tree: tree,
  };
};
/**
 * Builds the tree of categories and calculates its positions.
 * @private
 * @param {object} e Event object
 * @param {object} e.target The chart instance which the event was fired on.
 * @param {object[]} e.target.axes The axes of the chart.
 */
var onBeforeRender = function (e) {
  var chart = e.target,
    axes = chart.axes;
  axes
    .filter(function (axis) {
      return axis.options.type === "treegrid";
    })
    .forEach(function (axis) {
      var options = axis.options || {},
        labelOptions = options.labels,
        removeFoundExtremesEvent,
        uniqueNames = options.uniqueNames,
        numberOfSeries = 0,
        isDirty,
        data,
        treeGrid;
      // Check whether any of series is rendering for the first time,
      // visibility has changed, or its data is dirty,
      // and only then update. #10570, #10580
      // Also check if mapOfPosToGridNode exists. #10887
      isDirty =
        !axis.mapOfPosToGridNode ||
        axis.series.some(function (series) {
          return !series.hasRendered || series.isDirtyData || series.isDirty;
        });
      if (isDirty) {
        // Concatenate data from all series assigned to this axis.
        data = axis.series.reduce(function (arr, s) {
          if (s.visible) {
            // Push all data to array
            s.options.data.forEach(function (data) {
              if (isObject(data)) {
                // Set series index on data. Removed again after
                // use.
                data.seriesIndex = numberOfSeries;
                arr.push(data);
              }
            });
            // Increment series index
            if (uniqueNames === true) {
              numberOfSeries++;
            }
          }
          return arr;
        }, []);
        // setScale is fired after all the series is initialized,
        // which is an ideal time to update the axis.categories.
        treeGrid = getTreeGridFromData(
          data,
          uniqueNames,
          uniqueNames === true ? numberOfSeries : 1
        );
        // Assign values to the axis.
        axis.categories = treeGrid.categories;
        axis.mapOfPosToGridNode = treeGrid.mapOfPosToGridNode;
        axis.hasNames = true;
        axis.tree = treeGrid.tree;
        // Update yData now that we have calculated the y values
        axis.series.forEach(function (series) {
          var data = series.options.data.map(function (d) {
            return isObject(d) ? merge(d) : d;
          });
          // Avoid destroying points when series is not visible
          if (series.visible) {
            series.setData(data, false);
          }
        });
        // Calculate the label options for each level in the tree.
        axis.mapOptionsToLevel = getLevelOptions({
          defaults: labelOptions,
          from: 1,
          levels: labelOptions.levels,
          to: axis.tree.height,
        });
        // Collapse all the nodes belonging to a point where collapsed
        // equals true. Only do this on init.
        // Can be called from beforeRender, if getBreakFromNode removes
        // its dependency on axis.max.
        if (e.type === "beforeRender") {
          removeFoundExtremesEvent = H.addEvent(
            axis,
            "foundExtremes",
            function () {
              treeGrid.collapsedNodes.forEach(function (node) {
                var breaks = collapse(axis, node);
                axis.setBreaks(breaks, false);
              });
              removeFoundExtremesEvent();
            }
          );
        }
      }
    });
};
override(GridAxis.prototype, {
  init: function (proceed, chart, userOptions) {
    var axis = this,
      isTreeGrid = userOptions.type === "treegrid";
    // Set default and forced options for TreeGrid
    if (isTreeGrid) {
      // Add event for updating the categories of a treegrid.
      // NOTE Preferably these events should be set on the axis.
      addEvent(chart, "beforeRender", onBeforeRender);
      addEvent(chart, "beforeRedraw", onBeforeRender);
      userOptions = merge(
        {
          // Default options
          grid: {
            enabled: true,
          },
          // TODO: add support for align in treegrid.
          labels: {
            align: "left",
            /**
             * Set options on specific levels in a tree grid axis. Takes
             * precedence over labels options.
             *
             * @sample {gantt} gantt/treegrid-axis/labels-levels
             *         Levels on TreeGrid Labels
             *
             * @type      {Array<*>}
             * @product   gantt
             * @apioption yAxis.labels.levels
             *
             * @private
             */
            levels: [
              {
                /**
                 * Specify the level which the options within this object
                 * applies to.
                 *
                 * @type      {number}
                 * @product   gantt
                 * @apioption yAxis.labels.levels.level
                 *
                 * @private
                 */
                level: void 0,
              },
              {
                level: 1,
                /**
                 * @type      {Highcharts.CSSObject}
                 * @product   gantt
                 * @apioption yAxis.labels.levels.style
                 *
                 * @private
                 */
                style: {
                  /** @ignore-option */
                  fontWeight: "bold",
                },
              },
            ],
            /**
             * The symbol for the collapse and expand icon in a
             * treegrid.
             *
             * @product      gantt
             * @optionparent yAxis.labels.symbol
             *
             * @private
             */
            symbol: {
              /**
               * The symbol type. Points to a definition function in
               * the `Highcharts.Renderer.symbols` collection.
               *
               * @type {Highcharts.SymbolKeyValue}
               *
               * @private
               */
              type: "triangle",
              x: -5,
              y: -5,
              height: 10,
              width: 10,
              padding: 5,
            },
          },
          uniqueNames: false,
        },
        userOptions,
        {
          // Forced options
          reversed: true,
          // grid.columns is not supported in treegrid
          grid: {
            columns: void 0,
          },
        }
      );
    }
    // Now apply the original function with the original arguments,
    // which are sliced off this function's arguments
    proceed.apply(axis, [chart, userOptions]);
    if (isTreeGrid) {
      axis.hasNames = true;
      axis.options.showLastLabel = true;
    }
  },
  /**
   * Override to add indentation to axis.maxLabelDimensions.
   *
   * @private
   * @function Highcharts.GridAxis#getMaxLabelDimensions
   *
   * @param {Function} proceed
   *        The original function
   */
  getMaxLabelDimensions: function (proceed) {
    var axis = this,
      options = axis.options,
      labelOptions = options && options.labels,
      indentation =
        labelOptions && isNumber(labelOptions.indentation)
          ? options.labels.indentation
          : 0,
      retVal = proceed.apply(axis, argsToArray(arguments)),
      isTreeGrid = axis.options.type === "treegrid",
      treeDepth;
    if (isTreeGrid && this.mapOfPosToGridNode) {
      treeDepth = axis.mapOfPosToGridNode[-1].height;
      retVal.width += indentation * (treeDepth - 1);
    }
    return retVal;
  },
  /**
   * Generates a tick for initial positioning.
   *
   * @private
   * @function Highcharts.GridAxis#generateTick
   *
   * @param {Function} proceed
   *        The original generateTick function.
   *
   * @param {number} pos
   *        The tick position in axis values.
   */
  generateTick: function (proceed, pos) {
    var axis = this,
      mapOptionsToLevel = isObject(axis.mapOptionsToLevel)
        ? axis.mapOptionsToLevel
        : {},
      isTreeGrid = axis.options.type === "treegrid",
      ticks = axis.ticks,
      tick = ticks[pos],
      levelOptions,
      options,
      gridNode;
    if (isTreeGrid) {
      gridNode = axis.mapOfPosToGridNode[pos];
      levelOptions = mapOptionsToLevel[gridNode.depth];
      if (levelOptions) {
        options = {
          labels: levelOptions,
        };
      }
      if (!tick) {
        ticks[pos] = tick = new GridAxisTick(axis, pos, null, void 0, {
          category: gridNode.name,
          tickmarkOffset: gridNode.tickmarkOffset,
          options: options,
        });
      } else {
        // update labels depending on tick interval
        tick.parameters.category = gridNode.name;
        tick.options = options;
        tick.addLabel();
      }
    } else {
      proceed.apply(axis, argsToArray(arguments));
    }
  },
  /**
   * Set the tick positions, tickInterval, axis min and max.
   *
   * @private
   * @function Highcharts.GridAxis#setTickInterval
   *
   * @param {Function} proceed
   *        The original setTickInterval function.
   */
  setTickInterval: function (proceed) {
    var axis = this,
      options = axis.options,
      isTreeGrid = options.type === "treegrid";
    if (isTreeGrid) {
      axis.min = pick(axis.userMin, options.min, axis.dataMin);
      axis.max = pick(axis.userMax, options.max, axis.dataMax);
      fireEvent(axis, "foundExtremes");
      // setAxisTranslation modifies the min and max according to
      // axis breaks.
      axis.setAxisTranslation(true);
      axis.tickmarkOffset = 0.5;
      axis.tickInterval = 1;
      axis.tickPositions = this.mapOfPosToGridNode
        ? getTickPositions(axis)
        : [];
    } else {
      proceed.apply(axis, argsToArray(arguments));
    }
  },
});
override(GridAxisTick.prototype, {
  getLabelPosition: function (
    proceed,
    x,
    y,
    label,
    horiz,
    labelOptions,
    tickmarkOffset,
    index,
    step
  ) {
    var tick = this,
      lbOptions = pick(tick.options && tick.options.labels, labelOptions),
      pos = tick.pos,
      axis = tick.axis,
      options = axis.options,
      isTreeGrid = options.type === "treegrid",
      result = proceed.apply(tick, [
        x,
        y,
        label,
        horiz,
        lbOptions,
        tickmarkOffset,
        index,
        step,
      ]),
      symbolOptions,
      indentation,
      mapOfPosToGridNode,
      node,
      level;
    if (isTreeGrid) {
      symbolOptions =
        lbOptions && isObject(lbOptions.symbol) ? lbOptions.symbol : {};
      indentation =
        lbOptions && isNumber(lbOptions.indentation)
          ? lbOptions.indentation
          : 0;
      mapOfPosToGridNode = axis.mapOfPosToGridNode;
      node = mapOfPosToGridNode && mapOfPosToGridNode[pos];
      level = (node && node.depth) || 1;
      result.x +=
        // Add space for symbols
        symbolOptions.width +
        symbolOptions.padding * 2 +
        // Apply indentation
        (level - 1) * indentation;
    }
    return result;
  },
  renderLabel: function (proceed) {
    var tick = this,
      pos = tick.pos,
      axis = tick.axis,
      label = tick.label,
      mapOfPosToGridNode = axis.mapOfPosToGridNode,
      options = axis.options,
      labelOptions = pick(
        tick.options && tick.options.labels,
        options && options.labels
      ),
      symbolOptions =
        labelOptions && isObject(labelOptions.symbol)
          ? labelOptions.symbol
          : {},
      node = mapOfPosToGridNode && mapOfPosToGridNode[pos],
      level = node && node.depth,
      isTreeGrid = options.type === "treegrid",
      hasLabel = !!(label && label.element),
      shouldRender = axis.tickPositions.indexOf(pos) > -1,
      prefixClassName = "highcharts-treegrid-node-",
      collapsed,
      addClassName,
      removeClassName,
      styledMode = axis.chart.styledMode;
    if (isTreeGrid && node) {
      // Add class name for hierarchical styling.
      if (hasLabel) {
        label.addClass(prefixClassName + "level-" + level);
      }
    }
    proceed.apply(tick, argsToArray(arguments));
    if (isTreeGrid && node && hasLabel && node.descendants > 0) {
      collapsed = isCollapsed(axis, node);
      renderLabelIcon(tick, {
        color: !styledMode && label.styles.color,
        collapsed: collapsed,
        group: label.parentGroup,
        options: symbolOptions,
        renderer: label.renderer,
        show: shouldRender,
        xy: label.xy,
      });
      // Add class name for the node.
      addClassName = prefixClassName + (collapsed ? "collapsed" : "expanded");
      removeClassName =
        prefixClassName + (collapsed ? "expanded" : "collapsed");
      label.addClass(addClassName).removeClass(removeClassName);
      if (!styledMode) {
        label.css({
          cursor: "pointer",
        });
      }
      // Add events to both label text and icon
      [label, tick.labelIcon].forEach(function (object) {
        if (!object.attachedTreeGridEvents) {
          // On hover
          H.addEvent(object.element, "mouseover", function () {
            onTickHover(label);
          });
          // On hover out
          H.addEvent(object.element, "mouseout", function () {
            onTickHoverExit(label, labelOptions);
          });
          H.addEvent(object.element, "click", function () {
            tick.toggleCollapse();
          });
          object.attachedTreeGridEvents = true;
        }
      });
    }
  },
});
extend(
  GridAxisTick.prototype,
  /** @lends Highcharts.Tick.prototype */ {
    /**
     * Collapse the grid cell. Used when axis is of type treegrid.
     *
     * @see gantt/treegrid-axis/collapsed-dynamically/demo.js
     *
     * @private
     * @function Highcharts.Tick#collapse
     *
     * @param {boolean} [redraw=true]
     *        Whether to redraw the chart or wait for an explicit call to
     *        {@link Highcharts.Chart#redraw}
     */
    collapse: function (redraw) {
      var tick = this,
        axis = tick.axis,
        pos = tick.pos,
        node = axis.mapOfPosToGridNode[pos],
        breaks = collapse(axis, node);
      axis.setBreaks(breaks, pick(redraw, true));
    },
    /**
     * Expand the grid cell. Used when axis is of type treegrid.
     *
     * @see gantt/treegrid-axis/collapsed-dynamically/demo.js
     *
     * @private
     * @function Highcharts.Tick#expand
     *
     * @param {boolean} [redraw=true]
     *        Whether to redraw the chart or wait for an explicit call to
     *        {@link Highcharts.Chart#redraw}
     */
    expand: function (redraw) {
      var tick = this,
        axis = tick.axis,
        pos = tick.pos,
        node = axis.mapOfPosToGridNode[pos],
        breaks = expand(axis, node);
      axis.setBreaks(breaks, pick(redraw, true));
    },
    /**
     * Toggle the collapse/expand state of the grid cell. Used when axis is of
     * type treegrid.
     *
     * @see gantt/treegrid-axis/collapsed-dynamically/demo.js
     *
     * @private
     * @function Highcharts.Tick#toggleCollapse
     *
     * @param {boolean} [redraw=true]
     *        Whether to redraw the chart or wait for an explicit call to
     *        {@link Highcharts.Chart#redraw}
     */
    toggleCollapse: function (redraw) {
      var tick = this,
        axis = tick.axis,
        pos = tick.pos,
        node = axis.mapOfPosToGridNode[pos],
        breaks = toggleCollapse(axis, node);
      axis.setBreaks(breaks, pick(redraw, true));
    },
  }
);
// Make utility functions available for testing.
GridAxis.prototype.utils = {
  getNode: Tree.getNode,
};
